use std::fmt::{self, Debug, Display};
use std::io;
/// The error type for the NATS client, generic by the kind of error.
#[derive(Debug)]
pub struct Error<Kind>
where
    Kind: Clone + Debug + Display + PartialEq,
{
    pub kind: Kind,
    pub source: Option<crate::Error>,
}

impl<Kind> Error<Kind>
where
    Kind: Clone + Debug + Display + PartialEq,
{
    pub fn new(kind: Kind) -> Self {
        Self { kind, source: None }
    }

    pub fn with_source<S>(kind: Kind, source: S) -> Self
    where
        S: Into<crate::Error>,
    {
        Self {
            kind,
            source: Some(source.into()),
        }
    }

    // In some cases the kind doesn't implement `Copy` trait
    pub fn kind(&self) -> Kind {
        self.kind.clone()
    }
}

impl<Kind> Display for Error<Kind>
where
    Kind: Clone + Debug + Display + PartialEq,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(err) = &self.source {
            write!(f, "{}: {}", self.kind, err)
        } else {
            write!(f, "{}", self.kind)
        }
    }
}

impl<Kind> std::error::Error for Error<Kind>
where
    Kind: Clone + Debug + Display + PartialEq,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source
            .as_ref()
            .map(|boxed| boxed.as_ref() as &(dyn std::error::Error + 'static))
    }
}

impl<Kind> From<Kind> for Error<Kind>
where
    Kind: Clone + Debug + Display + PartialEq,
{
    fn from(kind: Kind) -> Self {
        Self { kind, source: None }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConnectErrorKind {
    /// Parsing the passed server address failed.
    ServerParse,
    /// DNS related issues.
    Dns,
    /// Failed authentication process, signing nonce, etc.
    Authentication,
    /// Server returned authorization violation error.
    AuthorizationViolation,
    /// Connect timed out.
    TimedOut,
    /// Erroneous TLS setup.
    Tls,
    /// Other IO error.
    Io,
    /// Reached the maximum number of reconnects.
    MaxReconnects,
}

impl Display for ConnectErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ServerParse => write!(f, "failed to parse server or server list"),
            Self::Dns => write!(f, "DNS error"),
            Self::Authentication => write!(f, "failed signing nonce"),
            Self::AuthorizationViolation => write!(f, "authorization violation"),
            Self::TimedOut => write!(f, "timed out"),
            Self::Tls => write!(f, "TLS error"),
            Self::Io => write!(f, "IO error"),
            Self::MaxReconnects => write!(f, "reached maximum number of reconnects"),
        }
    }
}

/// Returned when initial connection fails.
/// To be enumerate over the variants, call [ConnectError::kind].
pub type ConnectError = Error<ConnectErrorKind>;

impl From<io::Error> for ConnectError {
    fn from(err: io::Error) -> Self {
        ConnectError::with_source(ConnectErrorKind::Io, err)
    }
}

#[cfg(test)]
mod test {
    #![allow(dead_code)]

    use super::*;
    use std::fmt::Formatter;

    // Define a custom error kind as a public enum
    #[derive(Clone, Debug, PartialEq)]
    enum FooErrorKind {
        Bar,
        Baz,
    }

    impl Display for FooErrorKind {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            match self {
                Self::Bar => write!(f, "bar error"),
                Self::Baz => write!(f, "baz error"),
            }
        }
    }

    // Define a custom error type as a public struct
    type FooError = Error<FooErrorKind>;

    #[test]
    fn new() {
        let error = FooError::new(FooErrorKind::Bar);
        assert_eq!(error.kind, FooErrorKind::Bar);
        assert!(error.source.is_none());
    }

    #[test]
    fn with_source() {
        let source = std::io::Error::new(std::io::ErrorKind::Other, "foo");
        let error = FooError::with_source(FooErrorKind::Bar, source);
        assert_eq!(error.kind, FooErrorKind::Bar);
        assert_eq!(error.source.unwrap().to_string(), "foo");
    }

    #[test]
    fn kind() {
        let error: FooError = FooErrorKind::Bar.into();
        let kind = error.kind();
        // ensure the kind can be invoked multiple times even though Copy is not implemented
        let _ = error.kind();
        assert_eq!(kind, FooErrorKind::Bar);
    }

    #[test]
    fn display_without_source() {
        let error: FooError = FooErrorKind::Bar.into();
        assert_eq!(format!("{}", error), "bar error");
    }

    #[test]
    fn from() {
        let error: FooError = FooErrorKind::Bar.into();
        assert_eq!(error.kind, FooErrorKind::Bar);
        assert!(error.source.is_none());
    }
}
